package mil.af.amc.adpetracker.service;

import mil.af.amc.adpetracker.dao.*;
import mil.af.amc.adpetracker.model.Account;
import mil.af.amc.adpetracker.model.AccountAdpeAccount;
import mil.af.amc.adpetracker.model.AdpeAccount;
import mil.af.amc.adpetracker.model.Authority;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.mail.MailException;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.security.providers.encoding.MessageDigestPasswordEncoder;
import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UserDetailsService;
import org.springframework.security.userdetails.UsernameNotFoundException;

import java.util.*;

/**
 * Primary Service Implementation for Account objects
 *
 * @author TSgt Jason Ferguson
 */

public class AccountServiceImpl implements UserDetailsService, AccountService {

    private static final Log log = LogFactory.getLog(AccountServiceImpl.class);

    private AccountAdpeAccountDao accountAdpeAccountDao;

    public void setAccountAdpeAccountDao(AccountAdpeAccountDao accountAdpeAccountDao) {
        this.accountAdpeAccountDao = accountAdpeAccountDao;
    }

    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    private AdpeAccountDao adpeAccountDao;

    public void setAdpeAccountDao(AdpeAccountDao adpeAccountDao) {
        this.adpeAccountDao = adpeAccountDao;
    }

    private AuthorityDao authorityDao;

    public void setAuthorityDao(AuthorityDao authorityDao) {
        this.authorityDao = authorityDao;
    }

    private MailSender mailSender;

    public void setMailSender(MailSender mailSender) {
        this.mailSender = mailSender;
    }

    private RankDao rankDao;

    public void setRankDao(RankDao rankDao) {
        this.rankDao = rankDao;
    }

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
        Account acct = accountDao.getByUsername(username);
        if (acct == null) {
            throw new UsernameNotFoundException("Username not found.");
        }

        return acct;
    }

    public List<Account> getAll() {

        return accountDao.getAll();
    }

    public Account getById(int accountId) {

        return accountDao.getById(accountId);
    }

    public void createAccount(Map accountMap) {

        Account acct = new Account();

        acct.setUsername((String) accountMap.get("email"));
        acct.setFirstName((String) accountMap.get("firstName"));
        acct.setMiddleInitial((String) accountMap.get("middleInitial"));
        acct.setLastName((String) accountMap.get("lastName"));

        int rankId = (Integer) accountMap.get("rankId");
        acct.setRank(rankDao.getById(rankId));

        Authority auth = authorityDao.getByName("ROLE_USER");
        acct.getAccountAuthorities().add(auth);

        String randomString = randomString(8);
        acct.setPassword(generateHashedPassword(randomString, acct.getUsername()));

        acct.setEnabled(true);
        acct.setAccountNonExpired(true);
        acct.setAccountNonLocked(true);

        accountDao.saveOrUpdate(acct);

        StringBuilder msg = new StringBuilder();
        msg.append("You have created a new account in the ADPE Tracking application.\r\n\r\n");
        msg.append("Your new password is: ").append(randomString); // have to send the original here, not the hashed!

        try {
            SimpleMailMessage mail = new SimpleMailMessage();
            mail.setFrom("ADPETRACK-NOREPLY@scott.af.mil");
            mail.setTo(acct.getUsername());
            mail.setSubject("ADPE TRACKING SYSTEM Password Reset");
            mail.setText(msg.toString());
            mailSender.send(mail);
        } catch (MailException e) {
            log.debug("Failure to send email to newly registered user: " + acct.getUsername(), e);
        }
    }

    public void editAccount(Map accountMap) throws ServiceException {

        if (accountMap.get("accountId") == null) {
            throw new ServiceException("messages.account.noaccountid", "No AccountId found for object to edit");
        }

        int accountId = (Integer) accountMap.get("accountId");
        Account acct = accountDao.getById(accountId);

        // login
        acct.setUsername((String) accountMap.get("login"));

        // first name
        acct.setFirstName((String) accountMap.get("firstName"));

        // last name
        acct.setLastName((String) accountMap.get("lastName"));

        // middle initial
        acct.setMiddleInitial((String) accountMap.get("middleInitial"));

        // rank
        int rankId = (Integer) accountMap.get("rankId");
        acct.setRank(rankDao.getById(rankId));

        // adpe accounts

        Integer[] adpeAccountIds = (Integer[]) accountMap.get("adpeAccounts");
        for (int a : adpeAccountIds) {

            AccountAdpeAccount aaa = new AccountAdpeAccount();
            AdpeAccount adpeAccount = adpeAccountDao.getById(a);
            aaa.setAdpeAccount(adpeAccount);
            aaa.setAccount(acct);

            // Deal with the authorities Mr Account has here.
            Authority ecAuth = authorityDao.getByName("ROLE_EC");
            Authority adAuth = authorityDao.getByName("ROLE_ADMIN");
            if (acct.getAccountAuthorities().contains(ecAuth) || acct.getAccountAuthorities().contains(adAuth)) {
                aaa.setAuthority(ecAuth);
            }
            accountAdpeAccountDao.saveOrUpdate(aaa);

            acct.getAccountAdpeAccounts().add(aaa);

        }

        // authorities
        Integer[] authorityIds = (Integer[]) accountMap.get("authorities");

        Set<Authority> grantedAuthorities = new HashSet<Authority>();
        for (int a : authorityIds) {
            Authority auth = authorityDao.getById(a);
            grantedAuthorities.add(auth);
        }
        //pleasedonthaveaclasscastexceptionpleasedonthaveaclasscastexceptionpleasedonthaveaclasscastexception
        acct.setAccountAuthorities(grantedAuthorities);

        accountDao.saveOrUpdate(acct);
    }

    public void deleteAccount(int accountId) {

        Account acct = accountDao.getById(accountId);
        //acct.setAccountAdpeAccounts(new HashSet<AccountAdpeAccount>());
        acct.getAccountAdpeAccounts().clear();
        accountDao.delete(acct);
    }

    public void removeAdpeAccount(int accountId, int adpeAccountId) {

        Account acct = accountDao.getById(accountId);
        AdpeAccount adpeAccount = adpeAccountDao.getById(adpeAccountId);

        AccountAdpeAccount aaa = new AccountAdpeAccount(acct, adpeAccount);

        acct.getAccountAdpeAccounts().remove(aaa);
        accountDao.saveOrUpdate(acct);


    }

    /**
     * Generate a random alphanumeric string
     * // TODO: Need special characters!
     *
     * @param length    an int representing the length of the string to generate
     * @return      a random alphanumeric string of the passed length
     */
    protected static String randomString(int length) {

        Random randGen = new Random();

        char[] numbersAndLetters = ("0123456789abcdefghijklmnopqrstuvwxyz" +
                "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray();

        if (length < 1) {
            return null;
        }
        // Create a char buffer to put random letters and numbers in.
        char [] randBuffer = new char[length];
        for (int i = 0; i < randBuffer.length; i++) {
            randBuffer[i] = numbersAndLetters[randGen.nextInt(71)];
        }
        return new String(randBuffer);
    }

    public void changePassword(int accountId, String newPassword) {

        Account acct = accountDao.getById(accountId);
        acct.setPassword(generateHashedPassword(newPassword, acct.getUsername()));

        accountDao.saveOrUpdate(acct);

    }

    /**
     * Utility method to generate a SHA-256 hashed password based on a plaintext and a given salt
     *
     * @param password  String representing the password
     * @param salt      String representing the salt (should be the account username)
     * @return          String representing the hashed password
     */
    protected String generateHashedPassword(String password, String salt) {

        MessageDigestPasswordEncoder encoder = new MessageDigestPasswordEncoder("SHA-256");
        encoder.setEncodeHashAsBase64(true);
        return encoder.encodePassword(password, salt);

        //String hashedPassword = encoder.encodePassword(randomString, acct.getUsername());
    }

    public void resetPassword(String username) throws ServiceException {

        Account acct = accountDao.getByUsername(username);

        String randomPassword = randomString(9);
        String newPassword = generateHashedPassword(randomPassword, acct.getUsername());

                // Mail the original to the user
        StringBuilder msg = new StringBuilder();
        msg.append("You have requested that your password be reset in the ADPE Tracking application.\r\n\r\n");
        msg.append("Your new password is: ").append(randomPassword);

        try {
            SimpleMailMessage mail = new SimpleMailMessage();
            mail.setFrom("ADPETRACK-NOREPLY@scott.af.mil");
            mail.setTo(acct.getUsername());
            mail.setSubject("ADPE TRACKING SYSTEM Password Reset");
            mail.setText(msg.toString());
            mailSender.send(mail);
        } catch (MailException e) {

            // Egads! We can't send the message to tell the user their new password, so roll it back!
            //user.getAccount().setPassword(oldPassword);
            //userDao.save(user);

            throw new ServiceException("messages.userpassword.email.error", "Unable to send password reset email! Password not reset!");
        }

        acct.setPassword(newPassword);
    }
}
